local suit = require("lib.suit")
local camera = require("lib.hump.camera")
local smallfolk = require("lib.smallfolk")

local rand = love.math.random

local MS = {}

local ui = {
    menubox = { text = " Menu", checked = false },
    wrapbox = { text = " Wrap", checked = false },
    buttons = {},
    label = ""
}

for i = 3, 13, 2 do
    table.insert(ui.buttons, { w = i, h = i, text = i .. " x " .. i })
end

local image
local quads
local cam = camera()
local activeShader
local speedAngle = 90 / 0.2
local options = { w = 5, h = 5, wrap = false}
local matrix
local activeCount = 1

local saveFile = "save.dat"

local function getNearNode(rect, node, dx, dy, dir)
    local x = node.x + dx
    local y = node.y + dy
    if x < 1 or x > options.w then
        if options.wrap then
            x = (x + options.w - 1) % options.w + 1
        else
            return nil
        end
    end
    if y < 1 or y > options.h then
        if options.wrap then
            y = (y + options.h - 1) % options.h + 1
        else
            return nil
        end
    end
    local n = rect[y][x]
    n.dir = dir
    return n
end

local function getNearNodes(rect, node)
    local result = {}
    
    table.insert(result, getNearNode(rect, node, -1, 0, 1))
    table.insert(result, getNearNode(rect, node, 0, -1, 2))
    table.insert(result, getNearNode(rect, node, 1, 0, 3))
    table.insert(result, getNearNode(rect, node, 0, 1, 4))
    
    return result
end

local function getRandomNext(readys)
    local count = 0
    for _, node in ipairs(readys) do
        if not node.active then
            count = count + 1
        end
    end
    if count > 0 then
        local index = rand(count)
        for i, node in ipairs(readys) do
            if not node.active then
                index = index - 1
                if index == 0 then
                    return node
                end
            end
        end
    end
end

local function calculateImageAndAngle(node)
    if node.count == 1 then
        node.image = 1
        node.angle = node[1] and 0 or
                     node[2] and 90 or
                     node[3] and 180 or
                     270
                     
    elseif node.count == 3 then
        node.image = 4
        node.angle = not node[1] and 0 or
                     not node[2] and 90 or
                     not node[3] and 180 or
                     270
    elseif node.count == 2 then
        if node[1] == node[3] then
            node.image = 3
            node.angle = node[1] and 0 or 90
        else
            node.image = 2
            node.angle = node[1] and node[2] and 0 or
                         node[2] and node[3] and 90 or
                         node[3] and node[4] and 180 or
                         270
        end
    else
        node.image = 0
    end
    node.destAngle = node.angle
end

local function rotateNode(node, anim)
    node[1], node[2], node[3], node[4] = node[4], node[1], node[2], node[3]
    if not anim then
        node.angle = (node.angle + 90) % 360
        node.destAngle = node.angle
    else
        node.destAngle = node.destAngle + 90
    end
end

local function updateActive()
    for i = 1, options.h do
        for j = 1, options.w do
            matrix[i][j].active = false
        end
    end
    
    local center = matrix[(options.w + 1) / 2][(options.h + 1) / 2]
    center.active = true
    
    local stack = { center }
    local cur, nears, dir
    activeCount = 1
    while #stack > 0 do
        cur = stack[#stack]
        table.remove(stack, #stack)
        nears = getNearNodes(matrix, cur)
        for _, node in ipairs(nears) do
            dir = node.dir
            if not node.active and cur[dir] and node[(dir + 2 - 1) % 4 + 1] then
                node.active = true
                table.insert(stack, node)
                activeCount = activeCount + 1
            end
        end
    end
    ui.label = string.format("Progress %d/%d", activeCount, options.w * options.h)
end

local function updateCamera()
    cam:lookAt(options.w * 32 / 2, options.h * 32 / 2)
    MS:resize(love.graphics.getWidth(), love.graphics.getHeight())
end

local function generate(w, h, wrap)
    options.w = w
    options.h = h
    options.wrap = wrap
    
    local rect = {}
    for i = 1, h do
        rect[i] = {}
        for j = 1, w do
            rect[i][j] = { false, false, false, false, x = j, y = i, active = false, count = 0, angle = 0, image = 0 }
        end
    end
    
    local center = rect[(h+1)/2][(w+1)/2]
    center.active = true
    
    -- generate matrix
    local stack = { center }
    local selectIndex, cur, availables, direction, follow
    while #stack > 0 do
        selectIndex = rand(#stack)
        cur = stack[selectIndex]
        
        availables = getNearNodes(rect, cur)
        follow = getRandomNext(availables)
        if follow then
            direction = follow.dir
            
            cur[direction] = true
            cur.count = cur.count + 1
            
            follow[(direction + 2 - 1) % 4 + 1] = true
            follow.count = follow.count + 1
            if follow.count ~= 1 then
                print("ERROR")
            end
            follow.active = true
            table.insert(stack, follow)
            
            if cur.count == 3 then
                table.remove(stack, selectIndex)
            end
        else
            -- Unable to move, remove it
            table.remove(stack, selectIndex)
        end
    end
    
    for i = 1, h do
        for j = 1, w do
            local node = rect[i][j]
            calculateImageAndAngle(node)
            for k = 1, rand(3) do
                rotateNode(node)
            end
        end
    end
    
    return rect
end

function MS:enter()
    activeShader = activeShader or love.graphics.newShader([[
vec4 effect(vec4 color, sampler2D texture, vec2 texCoords, vec2 screenCoords) {
	vec4 texColor = texture2D(texture, texCoords);

	// NTSC weights
	float grey = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
    
    return vec4(0, 0 + 0.5, 0, texColor.a);
}
        ]])
    image = image or love.graphics.newImage("image.png")
    quads = quads or {
        love.graphics.newQuad(0, 0, 32, 32, 128, 32),
        love.graphics.newQuad(32, 0, 32, 32, 128, 32),
        love.graphics.newQuad(64, 0, 32, 32, 128, 32),
        love.graphics.newQuad(96, 0, 32, 32, 128, 32),
    }
    if love.filesystem.isFile(saveFile) then
        local save = smallfolk.loads(love.filesystem.read(saveFile))
        options = save.options
        ui.wrapbox.checked = options.wrap
        matrix = save.matrix
    else
        matrix = generate(options.w, options.h, options.wrap)
    end
    updateCamera()
    updateActive()
end

local function updateUI(dt)
    suit.layout:reset(20, 20)
    suit.Checkbox(ui.menubox, suit.layout:col(100, 30))
    
    suit.Label(ui.label, suit.layout:col(love.graphics.getWidth() - 200, 30))
    if activeCount == options.w * options.h then
        suit.Label("DONE", suit.layout:row())
    end
    
    if ui.menubox.checked then 
        suit.layout:reset(50, 60)
        suit.Checkbox(ui.wrapbox, suit.layout:row(100, 30))
        for i, opt in ipairs(ui.buttons) do
            suit.layout:row(100, 10)
            if suit.Button(opt.text, suit.layout:row(100, 30)).hit then
                matrix = generate(opt.w, opt.h, ui.wrapbox.checked)
                updateCamera()
                updateActive()
            end
        end
        suit.layout:row(100, 30)
        if suit.Button("Quit", suit.layout:row(100, 30)).hit then
            love.event.quit()
        end
    end
end

function MS:update(dt)
    updateUI(dt)
    
    local node
    for i = 1, options.w do
        for j = 1, options.h do
            node = matrix[i][j]
            if node.destAngle ~= node.angle then
                node.angle = node.angle + speedAngle * dt
                if node.angle >= node.destAngle then
                    node.angle = node.destAngle % 360
                    node.destAngle = node.angle
                end
            end
        end
    end
end

function MS:draw()
    cam:attach()
    
    local xoffset, yoffset
    love.graphics.setColor(255, 0, 0)
    love.graphics.setLineWidth(1)
    for i = 1, options.h + 1 do
        love.graphics.line(0, i * 32 - 32, options.w * 32, i * 32 - 32)
    end
    for i = 1, options.w + 1 do
        love.graphics.line(i * 32 - 32, 0, i * 32 - 32, options.h * 32)
    end
    
    local node
    for i = 1, options.h do
        for j = 1, options.w do
            xoffset = j * 32 - 32
            yoffset = i * 32 - 32
            node = matrix[i][j]
            if node.image > 0 and not node.active then
                love.graphics.draw(image, quads[node.image], 
                    xoffset + 16, yoffset + 16, 
                    node.angle / 180 * math.pi, 1, 1, 16, 16)
            end
        end
    end
    
    local oldShader = love.graphics.getShader()
    love.graphics.setShader(activeShader)
    for i = 1, options.h do
        for j = 1, options.w do
            xoffset = j * 32 - 32
            yoffset = i * 32 - 32
            node = matrix[i][j]
            if node.image > 0 and node.active then
                love.graphics.draw(image, quads[node.image], 
                    xoffset + 16, yoffset + 16, 
                    node.angle / 180 * math.pi, 1, 1, 16, 16)
            end
        end
    end
    love.graphics.setShader(oldShader)
    
    cam:detach()
    
    suit.draw()
end

function MS:resize(width, height)
    local tow = options.w * 32
    local toh = options.h * 32
    local min = math.floor(math.min((width - 150) / tow, (height - 150) / toh))
    cam:zoomTo(min)
end

function MS:mousepressed(x, y)
    local hitposx, hitposy = cam:worldCoords(x, y)
    local hitx, hity = math.ceil(hitposx / 32), math.ceil(hitposy / 32)
    if hitx >= 1 and hitx <= options.h and hity >= 1 and hity <= options.w then
        local node = matrix[hity][hitx]
        rotateNode(node, true)
        updateActive()
    end
end

function MS:quit()
    love.filesystem.write(saveFile, smallfolk.dumps{
            options = options,
            matrix = matrix
            })
end

return MS